package streaming;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

import simulation.ComparatorModulePeriodicUpdate;
import simulation.ComparatorModuleRequestList;
import simulation.Config;
import simulation.HostRequestedDiagnostic;
import simulation.Logs;
import simulation.Message;
import simulation.RequestedPacket;
import simulation.Simulator;

import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;

public class ComparatorModule {

    ArrayList<Integer> chunksToBeCompared = new ArrayList<Integer>();
    HashMap<Integer, RequestedPacket> mapRequestedChunks;
    Host host;
    ComparatorModulePeriodicUpdate periodicUpdate;
    ArrayList<ComparatorModuleRequestList> requestList;
    Integer ultimoCidComputado;
    Double nextMax;
    Integer nextCid;
    
    Entry<String, ArrayList<HostRequestedDiagnostic>> hrdMajority;
    
    private final String LOG = "ComparatorModule";
	private boolean identifiedSourceOnMajorityGroup;
	private boolean identifiedSourceOnNoMajorityGroup;
	private String chunkHashFromSource;

    ComparatorModule(Host host) {
    	this.mapRequestedChunks = new HashMap<Integer, RequestedPacket>();
    	this.host = host;
    	this.periodicUpdate = new ComparatorModulePeriodicUpdate(this);
    	this.requestList = new ArrayList<ComparatorModuleRequestList>();

    	// Request chunks that will be compared
    	////new MessageToTracker("requestChunksToBeCompared", this.getHost());
    	Simulator.getInstance().addIntDiagnosticRequestMessages();

    	this.updateStatus();

        Random r = new Random();

        String msgCompChunks = "Peer " +host.getId()+ " selected these chunks to compare: ";
        
        ultimoCidComputado = 0;
        while (ultimoCidComputado < Config.TIME * StreamingConfig.MCASTRATE) {
        	nextMax = (double)StreamingConfig.MCASTRATE * StreamingConfig.CHUNK_SELECTION_INTERVAL;
            nextCid = 1 + r.nextInt(nextMax.intValue());
            ultimoCidComputado += nextCid;
            if (ultimoCidComputado < Config.TIME * StreamingConfig.MCASTRATE) {
                this.chunksToBeCompared.add(ultimoCidComputado);
                msgCompChunks+= ultimoCidComputado+",";
            }
        }
        Logs.printMessage("CMConstructor", msgCompChunks);
    }

	public void addToRequestList(int cid, Host host){
		this.getRequestList().add(new ComparatorModuleRequestList(cid, host));
	}
	
	/**
	 * Send a message to a neighbor requesting a packet that it has available
	 * 
	 * @param source
	 *            The neighbor with packet that needs to be compared
	 * @param chunkId
	 *            Id of the chunk that is going to be requested
	 */
	public void requestPacket(Host source, int chunkId) {

		// Check if the chunk is already being controlled by comparator
		if (this.getMapRequestedChunks().get(chunkId) == null) {
			double simulationTime = Simulator.getInstance().getTime();

			// Determines the deadline to wait for all neighbors to answer
			// comparator request. The deadline is the disponibility window
			double timeout = simulationTime + StreamingConfig.MCASTWINDOWSIZE + (StreamingConfig.MCASTWINDOWSIZE * 0.1);

			RequestedPacket rp = new RequestedPacket(chunkId, timeout, host);

			// Inserts control to new chunk request
			this.getMapRequestedChunks().put(chunkId, rp);

			// After creating the structure to control the chunk, adds the
			// source, creating a new request because it has notified a chunk
			// that must be compared
			HostRequestedDiagnostic hrd = new HostRequestedDiagnostic(source.getId());
			hrd.setAnswered(false); // set as not answered

			// Adds new request
			this.getMapRequestedChunks().get(chunkId).getHostsList().put(source.getId(), hrd);

			// Sets chunk as requested
//			((Neighbor) this.getHost().getNeighborsMap().get(source.getId())).addRequestedPacket(chunkId);

			// Create message
			new Message("request", this.getHost(), source, chunkId, true);
//			Simulator.getInstance().addIntDiagnosticRequestMessages();
			
			Logs.printMessage("comparatorModule", "[CMP2a] Peer " + this.getHost().getId() + " sent a request to peer " + source.getId() + " asking for chunk " + chunkId);
		} else {
			// Check if the source have already had the chunk requested. If
			// doesn't contains the host, adds it
			if (!this.getMapRequestedChunks().get(chunkId).getHostsList().containsKey(source.getId())) {
				// Creates new request for the host that notified a chunk that
				// must be compared
				HostRequestedDiagnostic hrd = new HostRequestedDiagnostic(source.getId());
				hrd.setAnswered(false); // set as not answered

				// Adds new request
				this.getMapRequestedChunks().get(chunkId).getHostsList().put(source.getId(), hrd);

				// Sets chunk as requested in the neighbor data
//				((Neighbor) this.getHost().getNeighborsMap().get(source.getId())).addRequestedPacket(chunkId);

				// Create message
				new Message("request", this.getHost(), source, chunkId, true);
//				Simulator.getInstance().addIntDiagnosticRequestMessages();

				Logs.printMessage("comparatorModule", "[CMP2b] Peer " + this.getHost().getId() + " sent a request to peer "	+ source.getId() + " asking for chunk " + chunkId);
			}
		}
	}

	/**
	 * Handle packet received by the host.
	 * 
	 * @param chunkId
	 *            Packet sequence number
	 * @param source
	 *            Host that sent the packet
	 * @param data
	 *            Data of packet
	 */
	public void handlePacket(int chunkId, Host source, String data) {
		// check if structure does already exist. If not, create it
		if (this.getMapRequestedChunks().get(chunkId) == null) {
			double simulationTime = Simulator.getInstance().getTime();

			// Determines the deadline to wait for all neighbors to answer
			// comparator request. The deadline is the disponibility window
			double timeout = simulationTime + StreamingConfig.MCASTWINDOWSIZE + (StreamingConfig.MCASTWINDOWSIZE * 0.1);

			this.getMapRequestedChunks().put(chunkId,new RequestedPacket(chunkId, timeout, host));
		}

		// If has already sent U set to tracker, doesn't insert anymore
		if (!this.getMapRequestedChunks().get(chunkId).isSentToTracker()) {

			// Check if the source have already had the chunk requested. If
			// doesn't contains the host, adds it
			if (!this.getMapRequestedChunks().get(chunkId).getHostsList()
					.containsKey(source.getId())) {
				// Creates new request for the host that notified a chunk that
				// must be compared
				HostRequestedDiagnostic hrd = new HostRequestedDiagnostic(
						source.getId());

				// Adds new request, but this is an answered request
				this.getMapRequestedChunks().get(chunkId).getHostsList().put(
						source.getId(), hrd);
			}

			String hash = this.generateHash(data);

			// Check if chunk have already answered
			if (this.getMapRequestedChunks().get(chunkId).getHostsList().get(
					source.getId()).getChunkHash() != hash) {
				// Set chunk hash calculated over data sent by host
				this.getMapRequestedChunks().get(chunkId).getHostsList().get(
						source.getId()).setChunkHash(hash);

				// Sets chunk data received from source
				this.getMapRequestedChunks().get(chunkId).getHostsList().get(
						source.getId()).setChunkData(data);

				// Inserts host on a specific group based on the hash calculated
				this.getMapRequestedChunks().get(chunkId).insertUSet(
						this.getHost().getId(), source.getId(), hash);

				// Set that peer answered the request
				this.getMapRequestedChunks().get(chunkId).getHostsList().get(
						source.getId()).setAnswered(true);

				Logs.printMessage("cmp_handlePacket", "[CMP02] Peer " + this.getHost().getId() + " received chunk " + chunkId + 
						" from peer " + source.getId() + ". DATA = [" + data + "]. HASH: " + hash);
			}
		}
		
		// remove from neighbors outstanding list
//		Neighbor neighbor = (Neighbor) this.getHost().neighbors.get(source.getId());
//		if (neighbor.removeRequestedPacket(chunkId)){
//			System.out.println(chunkId + " removido com sucesso do vizinho de " + source.getId());
//		}
//		else {
//			System.out.println(chunkId + " não pode ser removido com sucesso do vizinho de " + source.getId());
//		}
	}

	/**
	 * Generate hash based on data received as argument
	 * 
	 * @param data
	 *            Data that will have hash value generated
	 * @return An array ob bytes containing the hash value
	 */
	public String generateHash(String data) {
		if (true){
			return data;
		}
		
		byte[] hash = {};
		MessageDigest md;
		String hashString = new String("");

		try {
			md = MessageDigest.getInstance("SHA");
			md.update(data.getBytes("8859_1"));
			hash = md.digest();

			// Convert byte array hash to String
			hashString = HexBin.encode(hash);

		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			System.out.println("Error: SHA Algorithm not found.");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		return hashString;
	}
	

	
	

	/**
	 * Update status of the structures, checking if timeout was reached. Also
	 * updates set U
	 */
	public void updateStatus() {
		/*
		Integer neighbors = 0;
		Integer limit = 0;
		Integer maxHash = 0;
		String stringMaxHash = "";
		Integer temp = 0;
		*/
		
		this.getPeriodicUpdate().addEvent();

//		this.selectRequests();
		
		for (RequestedPacket rp : this.getMapRequestedChunks().values()) {

			if (!rp.isSentToTracker() && rp.getTimeout() < Simulator.getInstance().getTime()){
				rp.finalizeGrouping();
				
				/*
				if(this.host.getId()  == 10) {
					neighbors = this.host.getNumberNeighbors();
					limit = (neighbors/2) + 1;
					for( String stringHash : rp.getUSet().keySet() ) {
						temp = rp.getUSet().get(stringHash).size();
						if (temp > maxHash) {
							maxHash = temp;
							stringMaxHash = stringHash;
						}
					}
					if(maxHash >= limit) {
						System.out.println("Houve diagnostico");
					} else {
						System.out.println("Nao foi possivel fazer diagnostico");
					}
					System.out.println("MaxHash = " + maxHash + " " + stringMaxHash);				
					System.out.println(rp.getUSet());
				}
				*/

				// Send message to tracker with set U
				//new MessageToTracker("uSet", rp.getCID(), this.getHost(), rp.getUSet());
				//Simulator.getInstance().addIntDiagnosticResponseMessages();
				
				//Glaucio - start - prints blacklisted hosts and put then in host
				//Logs.printMessage("comparatorModule", "[CMP03]  U(" + this.getHost().getId() + "," + rp.getCID() + " sent to Tracker ==> " + this.getUSets(rp.getCID()) + "\n");
				Host host = this.getHost();
				host.neighborIdsBlackListed = new ArrayList<Integer>();
				Integer cid = rp.getCID(); 
				String strUSets = this.getUSets(rp.getCID());
				List<Integer> hostsPollutedSubUSets = this.getHostsPollutedSubUSets(rp.getCID());
			    if ( hostsPollutedSubUSets.size()>0 ){
			    	Logs.printMessage("comparatorModule", "[CMP03]  U(" + host.getId() + "," + cid + ") of peer "+ host.getId() +" sent to Tracker ==> " + strUSets + " " +
			    			          ">>>>> POLLUTED NEIGHBORS: " + hostsPollutedSubUSets + (("Chunk " + cid + " original.").equals(hrdMajority.getKey()) ? " Acertou "  : " Errou ") +  "(hash majorit�rio: "+hrdMajority.getKey()+")"  );			    	
			    }else{
			    	Logs.printMessage("comparatorModule", "[CMP03]  U(" + host.getId() + "," + cid + ") of peer "+ host.getId() +" sent to Tracker ==> " + strUSets );
			    }
			    
			    host.neighborIdsBlackListed.addAll(hostsPollutedSubUSets);
				rp.setSentToTracker(true);
			
			}
		}
	}

	/**
	 * Tracker asks for U Set of chunk cid. Inserts cid on list of chunks to be
	 * compared NOTE: I don't create the structure for cid here only because of
	 * timeout. Host could receive the first chunk with cid a long time after
	 * the creation, then timeout would be small. Better is start timeout when
	 * receive the first notification of a neighbor informing it has the chunk
	 * with cid.
	 * 
	 * @param cid
	 *            Chunk id.
	 */
	public void handleTrackerChunksInfo(ArrayList<Integer> cids) {
		this.chunksToBeCompared = cids;
		Logs.printMessage("comparatorModule", "[CMP01]" + this.getHost().getId() + " recebeu lista de cids!!");
	}

	/**
	 * Print stats of U sets
	 */
	public void printUSets() {

		System.out.println("***** Peer " + this.getHost().getId() + " ***** \n");
		for (Integer key : mapRequestedChunks.keySet()) {
			RequestedPacket rp = mapRequestedChunks.get(key);

			System.out.println("U(" + key + ")");
			for (String hash : rp.getUSet().keySet()) {
				ArrayList<HostRequestedDiagnostic> hrdList = rp.getUSet().get(
						hash);

				System.out.println("Hash: " + hash);
				System.out.print("Peers: [");
				for (HostRequestedDiagnostic hrd : hrdList) {
					System.out.print(hrd.getHostId() + ",");
				}
				System.out.println("]");
			}
		}
	}
	
	public String getUSets(Integer cid) {
		String uSets = "";
		RequestedPacket rp = mapRequestedChunks.get(cid);

		uSets += "U(" + this.getHost().getId() + "," + cid + ") = {";
		for (String hash : rp.getUSet().keySet()) {
			ArrayList<HostRequestedDiagnostic> hrdList = rp.getUSet().get(
					hash);

			uSets += "(" + hash + ", {";
			for (HostRequestedDiagnostic hrd : hrdList) {
				uSets += hrd.getHostId() + ",";
			}
			uSets += "}),";
		}
		uSets += "}.";
		return uSets;
	}
	

	public HashMap<Integer, RequestedPacket> getMapRequestedChunks() {
		return mapRequestedChunks;
	}

	public RequestedPacket getRequestedPacketChunk(Integer cid) {
		return mapRequestedChunks.get(cid);
	}
	
	public void setMapRequestedChunks(
			HashMap<Integer, RequestedPacket> mapRequestedChunks) {
		this.mapRequestedChunks = mapRequestedChunks;
	}

	public Host getHost() {
		return host;
	}

	public void setHost(Host host) {
		this.host = host;
	}

	public ComparatorModulePeriodicUpdate getPeriodicUpdate() {
		return periodicUpdate;
	}

	public ArrayList<Integer> getHostIdListFromSetU(Integer cid) {
		ArrayList<Integer> hostIdList = new ArrayList<Integer>();
		
		RequestedPacket rp = mapRequestedChunks.get(cid);
		if (rp != null) {
			if (rp.getCID() != null){
				HashMap<String, ArrayList<HostRequestedDiagnostic>> uSet = rp.getUSet();
				for( String stringHash : uSet.keySet() ) {
					for( HostRequestedDiagnostic hostInUSet : uSet.get(stringHash)) {
						hostIdList.add(hostInUSet.getHostId());
					}
				}
			} 
		}
  
		 return hostIdList;
	}
	
	public void setPeriodicUpdate(ComparatorModulePeriodicUpdate periodicUpdate) {
		this.periodicUpdate = periodicUpdate;
	}

	public ArrayList<ComparatorModuleRequestList> getRequestList() {
		return requestList;
	}

	public void setRequestList(ArrayList<ComparatorModuleRequestList> requestList) {
		this.requestList = requestList;
	}
	
	
	//Glaucio - list hosts that seams to be polluted
	@SuppressWarnings({ "unchecked" })
	public List<Integer> getHostsPollutedSubUSets(Integer cid) {
		
		RequestedPacket rp = mapRequestedChunks.get(cid);
		List<Integer> hostIdsPolluted = new ArrayList<Integer>();

//		if (rp.getUSet().size()>2){
//			Logs.printMessage("comparatorModule","Oops: U has more than 2 hash values!!");
//		}
		if (rp.getUSet().size() > 1){			
			LinkedHashMap<String, ArrayList<HostRequestedDiagnostic>> sortedUSetValues = sortUSetsByValueSize(rp.getUSet());
			sortedUSetValues.remove("Haven't answered");	
			Iterator<Entry<String, ArrayList<HostRequestedDiagnostic>>> it = sortedUSetValues.entrySet().iterator();		
			
			// discard bigger for a while
			hrdMajority = it.next();
			chunkHashFromSource = "";
			identifiedSourceOnMajorityGroup = false;
			Integer sourceId = 0;
			
			String strUSets = this.getUSets(rp.getCID());
			
			for (HostRequestedDiagnostic hrd : hrdMajority.getValue()){
				if (hrd.getHostId()==sourceId){				
					chunkHashFromSource = hrd.getChunkHash();
					identifiedSourceOnMajorityGroup = true;
				}
			}
			
			// Majority Entry has Source. So, other Entries have polluted peers 
			if (identifiedSourceOnMajorityGroup){
				// get others (they must have to be polluted)
				Logs.printMessage("comparatorModule", "[CMP04]  U(" + host.getId() + "," + cid + ") of peer "+ host.getId() +" has Source Peer on Majority Group! Other groups has been identified as polluted."  );
				for (int i=1; i<sortedUSetValues.size(); i++){
					Entry<String, ArrayList<HostRequestedDiagnostic>> hrdEntry = it.next();
					if ( hrdEntry.getValue().size()<hrdMajority.getValue().size() ){
						for (HostRequestedDiagnostic hrd : hrdEntry.getValue()){
							hostIdsPolluted.add(hrd.getHostId());						
						}
					}
				}
				return hostIdsPolluted; //confirmed polluted
			}
			
			
			identifiedSourceOnNoMajorityGroup = false;
			// get others (they seems to be polluted)
			for (int i=1; i<sortedUSetValues.size(); i++){
				Entry<String, ArrayList<HostRequestedDiagnostic>> hrdEntry = it.next();
				if ( hrdEntry.getValue().size()<hrdMajority.getValue().size() ){
					for (HostRequestedDiagnostic hrd : hrdEntry.getValue()){
						if (hrd.getHostId()==sourceId){
							chunkHashFromSource = hrd.getChunkHash();
							identifiedSourceOnNoMajorityGroup = true;							
						}
						hostIdsPolluted.add(hrd.getHostId());						
					}
				}
			}
			
			// NoMajority Entry has Source
			it = sortedUSetValues.entrySet().iterator();
			if (identifiedSourceOnNoMajorityGroup){
				Logs.printMessage("comparatorModule", "[CMP04]  U(" + host.getId() + "," + cid + " of peer "+ host.getId() +" has Source Peer on NOMajority Group! Other groups has been identified as polluted."  );
				hostIdsPolluted = new ArrayList<Integer>();
				for (int i=0; i<sortedUSetValues.size(); i++){
					Entry<String, ArrayList<HostRequestedDiagnostic>> hrdEntry = it.next();
					if ( hrdEntry.getValue().size()<hrdMajority.getValue().size() ){
						for (HostRequestedDiagnostic hrd : hrdEntry.getValue()){
							if (!hrd.getChunkHash().equals(chunkHashFromSource)){
								hostIdsPolluted.add(hrd.getHostId());							
							}												
						}
					}
				}
			}
			
		}
		return hostIdsPolluted;
	}
	


	//Glaucio - sort USets by size
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public LinkedHashMap sortUSetsByValueSize( HashMap passedMap) {
		List mapKeys = new ArrayList(passedMap.keySet());
		   List mapValues = new ArrayList(passedMap.values());
		   
		   Collections.sort(mapValues, new Comparator(){
				 
	            public int compare(Object o1, Object o2) {
	            	ArrayList<HostRequestedDiagnostic> p1 = (ArrayList<HostRequestedDiagnostic>) o1;
	            	ArrayList<HostRequestedDiagnostic> p2 = (ArrayList<HostRequestedDiagnostic>) o2;
	               return ((Integer)p2.size()).compareTo((Integer)p1.size());
	            }
	        });
		   
		   Collections.sort(mapKeys);

		   LinkedHashMap<String, ArrayList<HostRequestedDiagnostic>> sortedMap = new LinkedHashMap<String, ArrayList<HostRequestedDiagnostic>>();

		   Iterator valueIt = mapValues.iterator();
		   while (valueIt.hasNext()) {
		       Object val = valueIt.next();
		    Iterator keyIt = mapKeys.iterator();

		    while (keyIt.hasNext()) {
		        Object key = keyIt.next();
		        String comp1 = passedMap.get(key).toString();
		        String comp2 = val.toString();

		        if (comp1.equals(comp2)){
		            passedMap.remove(key);
		            mapKeys.remove(key);
		            sortedMap.put((String)key, (ArrayList<HostRequestedDiagnostic>)val);
		            break;
		        }

		    }

		}
		return sortedMap;
	}
}